package host.routingProtocol;

import homeMadeExceptions.NetworkException;
import homeMadeExceptions.ScatterNetException;
import host.RoutingNetworkSocket;

import java.util.HashMap;
import java.util.Vector;

import datatypes.BluetoothDeviceAddress;
import datatypes.Host;
import datatypes.LinkMode;
import datatypes.Message;
import datatypes.RoutingObject;

public class CopyOfStandardRoutingProtocol extends RoutingNetworkSocket {

	
	/**
	 * This Standard Routing Protocol is for simulation of Discovery Protocols on homogenous Networks.
	 * 
	 * It works in this way:
	 * 
	 * 1. Device is turned on
	 * 	1.1 Set local Linkmode to NONE
	 * 	1.2 Set UP RFCOMM Server Socket
	 *  1.3 Set UP RFCOMM Client Socket
	 * 2. Inquiry Scan, for find other Devices are started
	 * 3. Try to connect Device next to mine (only possible if other device is RFCOMM_Server)
	 * 	3.1 if connection failed, try again with next possible Device (if connection failed, the Device don't know the protocol or has max Connections) 
	 *  3.2 if no connection is possible do nothing mor, wait for incoming connection
	 * 4. Ask new Connector for all connected (directly an not directly) devices
	 * 5. If there are devices in Range, not in List, try to connect these devices, too . Do so also with exchange of devicelist
	 * 6. If there is a new incoming Connection, request Devicelist from new Connected Host
	 */
	private static final String SEARCH_PHRASE = "Search";
	private static final String SEARCH_ANSWER = "sAnswer";
	private static final String ROUTING_INFORMATION_QUERY  = "routingInformationQuery";
	private static final String ROUTING_INFORMATION_ANSWER = "AnswerForInformationQuery";
	
	private RoutingTable rT = new RoutingTable();
	private HashMap<Integer, Message> sendMessages = new HashMap<Integer,Message>();
	private HashMap<Integer, RoutingObject> openRequests = new HashMap<Integer,RoutingObject>();
	
	private Vector<BluetoothDeviceAddress> inquiryCache = null;
	private int connectionRequestStatus = -1;
	
	
	private CopyOfStandardRoutingProtocol (Host host){
		super(host);
	}
	
	public static CopyOfStandardRoutingProtocol getSocket(Host host){
		return new CopyOfStandardRoutingProtocol(host);
	}
	
	public void enableBluetoothDevice (){
		super.enableBluetoothDevice();
		super.hciConfig_setLM(LinkMode.NONE);
		try {
			super.getClientSocket();
			super.getServerSocket();
		} catch (NetworkException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		super.hciTool_inq();
	}
	
	public void newRemoteHostConnected (BluetoothDeviceAddress remoteHost) {
		rT.addNewDeviceToRoutingTable(remoteHost, null);
		this.host.getDiscoverySocket().newConnectionEstablished(remoteHost);
	}
	
	/**
	 * overrides a Method from RoutingNetworkSocket
	 * 
	 * this method is invoked from Miniworld if the result of the connectionprocess of the device is known
	 * in real life, the bluetooth device itself will send this signal, if a connection could be established
	 */
	public void rfcommConnectionRequestReply (BluetoothDeviceAddress pairingPartner, boolean success){
		if (success){
			rT.addNewDeviceToRoutingTable(pairingPartner, null);
			this.askForRoutingTableInformations(pairingPartner);
		} else {
			this.startConnectingProcedure();
		}
	}
	
	private void askForRoutingTableInformations (BluetoothDeviceAddress connectedDevice){
		RoutingObject object = new RoutingObject(connectedDevice, null, ROUTING_INFORMATION_QUERY);
		object.addHopToRoute(host.getBd_Addr());
		Message rM = new Message(object, false);
		this.invokeSendMessage(rM, connectedDevice);
	}
	
	//an Message could not be send to target (or it is not clear if it could be send)
	public void invokeMsgLostSignal(BluetoothDeviceAddress target, Message msg) {
		this.host.getDiscoverySocket().invokeMessageLost(msg, target);
	}

	public void invokeMsgAckSignal(Message msg, BluetoothDeviceAddress target) {
		this.host.getDiscoverySocket().invokeAckForSendedMessage(msg, target);
	}
	
	//This method is only called by discovery Layer and overrides the Method from Socket
	public void invokeSendMessage(Message msg, BluetoothDeviceAddress target){
		if (msg.isDiscoveryMessage()){
			RoutingObject tmp = new RoutingObject(target, msg);
			tmp.addHopToRoute(host.getBd_Addr());
			//if Target is unknown search for Host and than send or raise Exception
			if (rT.getNextHop(target)== null){
				this.searchForHost(target, tmp);
			} else {
				Message rM = new Message(tmp, false);
				super.invokeSendMessage(rM, rT.getNextHop(target));				
			}
		} else {
			if (rT.getNextHop(target) != null){
				super.invokeSendMessage(msg, rT.getNextHop(target));				
			}
		}
	}
	
	
	
	//sends search Message to all neighbours
	private void searchForHost (BluetoothDeviceAddress bd_addr, RoutingObject rO){
		for (BluetoothDeviceAddress target : rT.getNeighbors()){
			RoutingObject searchObject = new RoutingObject(bd_addr, null, SEARCH_PHRASE);
			searchObject.addHopToRoute(host.getBd_Addr());
			Message rM = new Message(searchObject, false);
			this.openRequests.put(rM.getMsgID_asInteger(), rO);
				//every Message needs his own ID
			super.invokeSendMessage(rM, target);
			this.sendMessages.put(rM.getMsgID_asInteger(),rM);
		}
	}
	
	/**
	 * is Flooding rm to all neighbors not yet affected by the route
	 * 
	 * if Message was sent to minimal one neighbor, this method returns true
	 * otherwise false
	 * 
	 * @param rM
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private boolean intelligentFlooooooodToNeighbor (RoutingObject rO, int msgID){
		boolean sended = false;
		Vector<BluetoothDeviceAddress> tmp = (Vector<BluetoothDeviceAddress>)rT.getNeighbors().clone();
		for (BluetoothDeviceAddress locAdd : rO.getRoute()){
			if (tmp.contains(locAdd)){
				tmp.remove(locAdd);
			}
		}
		for (BluetoothDeviceAddress target: tmp){
			RoutingObject nRoutingObj = createNewRoutingObject (rO);
			Message nM = new Message(nRoutingObj, false, msgID);
			super.invokeSendMessage(nM, target);
			this.sendMessages.put(nM.getMsgID_asInteger(), nM);	
			sended = true;
		}
		return sended;
	}
	
	
	private RoutingObject createNewRoutingObject(RoutingObject ro) {
		RoutingObject newRO = new RoutingObject(ro.getTarget(), ro.getMessage(), ro.getType());
		Vector<BluetoothDeviceAddress> route = (Vector<BluetoothDeviceAddress>) ro.getRoute().clone();
		newRO.setRoute(route);
		return newRO;
	}

	public void  onInquiryScanAnswer (Vector<BluetoothDeviceAddress> answer) {
		this.inquiryCache = answer;	
		this.connectionRequestStatus = 0;
		this.startConnectingProcedure();
	}
	
	
	//This method overrides the method from the RoutingNetworkSocket
	@SuppressWarnings("unchecked")
	public void onReceiveMessage(Message msg, BluetoothDeviceAddress sender){
		System.out.println("recevied");
		if (msg.isRoutingMessage()){
			RoutingObject tmp = (RoutingObject)msg.getContent();
			//at first update Route Informations
			rT.updateDistanceVector(tmp.getRoute());
			//I'm the Target
			if (tmp.getTarget().getAddress().equals(this.host.getBd_Addr().getAddress())){
				//Message with Discovery Protocol Informations
				if (tmp.getDiscoveryMessage() != null){
					super.onReceiveMessage(tmp.getDiscoveryMessage(), tmp.getInitialSender());
				} 
				//Message only with Routing Protocol Informations
				else {
					if (tmp.getType() != null){
						if (tmp.getType().equals(SEARCH_PHRASE)){
							//Answer, than target becomes clear Information
							RoutingObject newRO = new RoutingObject(tmp.getInitialSender(), msg, SEARCH_ANSWER);
							newRO.addHopToRoute(tmp.getTarget());
							Message tmpMsg = new Message(newRO, false);
							super.invokeSendMessage(tmpMsg, rT.getNextHop(tmp.getInitialSender()));
						} else if (tmp.getType().equals(SEARCH_ANSWER)){
							//FIXME???
						} else if (tmp.getType().equals(ROUTING_INFORMATION_QUERY)){
							sendRoutingTableInformations (tmp.getInitialSender());
						} else if (tmp.getType().equals(ROUTING_INFORMATION_ANSWER)){
							Message x = tmp.getRoutingMessage();
							HashMap<String, Vector<BluetoothDeviceAddress>> routeMap = (HashMap<String, Vector<BluetoothDeviceAddress>>) x.getContent();
							for (String content: routeMap.keySet()){
								Vector<BluetoothDeviceAddress> route = routeMap.get(content);
								route.add(tmp.getInitialSender());
								rT.updateDistanceVector(route);
							}
							continueConnectionProcedure();
						}
					} else {
					//no further Informations needed
					}
				}		
			} 
			else if (tmp.getTarget().getAddress().equals(BluetoothDeviceAddress.getBroadcastAddresse())){
				//Message with Discovery Protocol Informations
				if (tmp.getDiscoveryMessage() != null){
					super.onReceiveMessage(tmp.getDiscoveryMessage(), tmp.getInitialSender());
				} 
				if (tmp.getType().equals(SEARCH_PHRASE)){
					//i don't know the host too
					if (rT.getNextHop(tmp.getTarget())==null || BluetoothDeviceAddress.getBroadcastAddresse().isEqual(tmp.getTarget())){
						//send in an intelligent way to my neighbors not in Route, if I don't send the Message before
						if (this.sendMessages.get(msg.getMsgID_asInteger())!= null){
							//ignore Message (schon weitergeleitet)
						} else {
							this.intelligentFlooooooodToNeighbor((RoutingObject)msg.getContent(), msg.getMsgID());
						}
					}
				}
			}
			//I'm not the Target
			else {
				//add me to route an send to next hop
				tmp.addHopToRoute(host.getBd_Addr());
				
				if (tmp.getType().equals(SEARCH_PHRASE)){
					//i don't know the host too
					if (rT.getNextHop(tmp.getTarget())==null || BluetoothDeviceAddress.getBroadcastAddresse().isEqual(tmp.getTarget())){
						//send in an intelligent way to my neighbors not in Route, if I don't send the Message before
						if (this.sendMessages.get(msg.getMsgID_asInteger())!= null){
							//ignore Message (schon weitergeleitet)
						} else {
							this.intelligentFlooooooodToNeighbor((RoutingObject)msg.getContent(), msg.getMsgID());
						}
					}
				//Message is not SEARCH_PHRASE
				} else {
					//If target is unknown
					if (rT.getNextHop(tmp.getTarget())==null){
						//ignore
					} 
					//The Target and the shortest path is known
					else {
						super.invokeSendMessage(msg, rT.getNextHop(tmp.getTarget()));			
						this.sendMessages.put(msg.getMsgID_asInteger(), msg);
					}					
				}
			}
		} else {
			//Must be an other protocol, the host don't understand
			//this method sends the Received Message to discovery "Layer" if it is an discoveryMessage
			//super.onReceiveMessage(msg, sender);	
		}		
	}

	private void startConnectingProcedure (){
		if (connectionRequestStatus > -1 && connectionRequestStatus < inquiryCache.size()){
			try {
				super.rfcomm_connect_to_server(this.inquiryCache.get(connectionRequestStatus));
				connectionRequestStatus ++ ;
			} catch (ScatterNetException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	private void continueConnectionProcedure() {
		
		System.out.println("continue");
		if (connectionRequestStatus > -1 && connectionRequestStatus < inquiryCache.size()){
			try {
				while (connectionRequestStatus > -1 && connectionRequestStatus < inquiryCache.size()) {
					if (!rT.isRouteKnown(this.inquiryCache.get(connectionRequestStatus))) {
						super.rfcomm_connect_to_server(this.inquiryCache.get(connectionRequestStatus));
						connectionRequestStatus++;
						break;
					} else {
						connectionRequestStatus++;						
					}
				}
			} catch (ScatterNetException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	private void sendRoutingTableInformations(BluetoothDeviceAddress initialSender) {
		//Answer, than target becomes clear Information
		RoutingObject newRO = new RoutingObject(initialSender, new Message(rT.getAllKnownRoutes(),false), ROUTING_INFORMATION_ANSWER);
		newRO.addHopToRoute(this.host.getBd_Addr());
		Message tmpMsg = new Message(newRO, false);
		super.invokeSendMessage(tmpMsg, rT.getNextHop(initialSender));
	}
	
	public Vector<BluetoothDeviceAddress> getCurrentConnections (){
		return rfcomm_show();
	}
	
	public boolean isDeviceStillConnected(){
		return rfcomm_show().size() > 0;
	}

	class RoutingTable {
		private HashMap <String, BluetoothDeviceAddress> nextHop = new HashMap <String, BluetoothDeviceAddress>();
		private HashMap <String, Vector<BluetoothDeviceAddress>> routeList = new HashMap <String, Vector<BluetoothDeviceAddress>>();
		private HashMap <String, Integer> distanceVector = new HashMap <String, Integer>();
		private Vector<BluetoothDeviceAddress> directNeighbor = new Vector<BluetoothDeviceAddress>();
		
		public BluetoothDeviceAddress getNextHop(BluetoothDeviceAddress receiver){
			return nextHop.get(receiver.getAddress());
		}
		
		/**
		 * within an Routing Message an Routing-List came from the device
		 * with bd_addr and the Message comes along the way "route"
		 * --> distance from bd_addr is 1 Hop
		 * --> distance from source is 1 + routelength
		 * --> route is neighbour=last element -> 1 hop = last element - 1 -> ...
		 * 
		 * the DV will beUpdated, if the Route is shorter than the known
		 */
		public void updateDistanceVector (Vector<BluetoothDeviceAddress> route){
			if (!route.lastElement().getAddress().equals(host.getBd_Addr().getAddress())){
				//add bd_addr to neighbourhood-
				if (!distanceVector.containsKey(route.lastElement().getAddress())){
					this.addNewDeviceToRoutingTable(route.lastElement(), null);
				} else {
					//if new way is shorter oldvalue < newvalue = last - i
					if (distanceVector.get(route.lastElement().getAddress()).intValue() > 1){
						this.exchangeDeviceInRoutingTable(route.lastElement(), null);
					}
				}
				for (int i = route.size()-2; i >= 0; i--){
						if(!distanceVector.containsKey(route.get(i).getAddress())){
							this.addNewDeviceToRoutingTable(route.get(i), route.get(i+1));
						} else {
							//if new way is shorter oldvalue < newvalue = last - i
							if (distanceVector.get(route.get(i).getAddress()).intValue() > (route.size()-1)-i){
								this.exchangeDeviceInRoutingTable(route.get(i), route.get(i+1));
							}
						}
				}				
			} 
		}
		
		
		/**
		 * This method is for update the Routing-Table. First Parameter is the new Host and second Parameter is the connected Neighbor over which the Message should be send.
		 * @param exchangeEntry
		 * @param overNode
		 */
		private void exchangeDeviceInRoutingTable (BluetoothDeviceAddress exchangeEntry, BluetoothDeviceAddress overNode){
				this.distanceVector.remove(exchangeEntry.getAddress());
				this.nextHop.remove(exchangeEntry.getAddress());
				this.routeList.remove(exchangeEntry.getAddress());
				this.directNeighbor.remove(exchangeEntry);
				this.addNewDeviceToRoutingTable(exchangeEntry, overNode);
		}
		/**
		 * overNode must be known , if it is null new Neighbor is added
		 * 
		 * @param newEntry 
		 * @param overNode should be known or is null if it is a new Neighbor
		 */
		@SuppressWarnings("unchecked")
		private void addNewDeviceToRoutingTable (BluetoothDeviceAddress newEntry, BluetoothDeviceAddress overNode){
			if (overNode == null){
				nextHop.put(newEntry.getAddress(), newEntry);
				distanceVector.put(newEntry.getAddress(), new Integer(1));
				Vector<BluetoothDeviceAddress> route = new Vector<BluetoothDeviceAddress>();
				route.add(newEntry);
				routeList.put(newEntry.getAddress(), route);
				this.directNeighbor.add(newEntry);
			} else {
				nextHop.put(newEntry.getAddress(), nextHop.get(overNode));
				distanceVector.put(newEntry.getAddress(), new Integer(distanceVector.get(overNode.getAddress()).intValue() + 1));
				Vector<BluetoothDeviceAddress> route = new Vector<BluetoothDeviceAddress>();
				route = (Vector<BluetoothDeviceAddress>)routeList.get(overNode.getAddress()).clone();
				route.add(overNode);
				routeList.put(newEntry.getAddress(), route);
			}
		}
		
		public Vector<BluetoothDeviceAddress> getNeighbors (){
			return this.directNeighbor;
		}
		
		/**
		 * This method is for asking for an Host, and returns true if a route to the destination is known
		 * @return
		 */
		public boolean isRouteKnown (BluetoothDeviceAddress destination){
			return nextHop.get(destination.getAddress()) != null;
		}
		
		/**
		 * for getting an List of all Devices reachable over network (that stored in the Routing Table)
		 * @return
		 */
		@SuppressWarnings("unchecked")
		public HashMap<String, Vector<BluetoothDeviceAddress>> getAllKnownRoutes(){
			Vector<String> tmp = new Vector<String>();
			for (String x : nextHop.keySet()){
				tmp.add(x);
			}
			return (HashMap<String, Vector<BluetoothDeviceAddress>>)routeList.clone();
		}
		
		public int getNumberOfHops (BluetoothDeviceAddress target){
			if (this.distanceVector.get(target.getAddress()) == null || this.routeList.get(this.distanceVector.get(target.getAddress())) == null){
				return -1;
			} else {
				return this.routeList.get(this.distanceVector.get(target.getAddress())).size();
			}
		}
		
		private int getNumberOfHops (String target){
			if (this.distanceVector.get(target) == null || this.routeList.get(this.distanceVector.get(target)) == null){
				return -1;
			} else {
				return this.routeList.get(this.distanceVector.get(target)).size();
			}
		}
	}
	
}
